package ch.fhnw.ams.sudoku.gui;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Toolkit;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;

import javax.swing.Action;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JMenuBar;
import javax.swing.JOptionPane;
import javax.swing.filechooser.FileFilter;

import org.apache.log4j.Logger;
import org.divxdede.swing.busy.JBusyComponent;

import ch.fhnw.ams.sudoku.Globals;
import ch.fhnw.ams.sudoku.SudokuGame;
import ch.fhnw.ams.sudoku.SudokuGame.SudokuState;
import ch.fhnw.ams.sudoku.SudokuGrid;
import ch.fhnw.ams.sudoku.SudokuObserver;
import ch.fhnw.ams.sudoku.io.SudokuExporter;
import ch.fhnw.ams.sudoku.io.SudokuExporter.ImageFormat;
import ch.fhnw.ams.sudoku.io.UserProperties;

import com.sun.jaf.ui.ActionManager;
import com.sun.jaf.ui.UIFactory;


/**
 * The main frame of the Sudoku. This class contains the sudoku gird graphic,
 * control panel and the menu bar. If the state of the game changes the observer
 * method is called.
 * 
 * @author amsler
 * 
 */
public class SudokuFrame extends JFrame implements SudokuObserver{
	
	private static final long serialVersionUID = 1L;
	private static final Logger log = Logger.getLogger(SudokuFrame.class);
	
	private final SudokuGame sudokuGame;
	private final GridPanel gridPanel;
	private JBusyComponent<GridPanel> busyComponent;
	private final UserProperties userProp;
	private ActionManager actionManager;
	private JFileChooser fileChooser;
	private JFileChooser exportChooser;
	private final ActionController actionController;
	private ControlPanel controlPanel;
	
	private Action playAction;
	private Action clearAction;
	private Action editAction;
	private Action checkAction;
	private Action solveAction;
	private Action resetAction;
	
	
	/**
	 * Creates a new Sudoku SideKick frame.
	 */
	public SudokuFrame(SudokuGame sudokuGame) {
		super(Globals.APP_NAME + " " + Globals.APP_VERSION);
		this.sudokuGame = sudokuGame;
		userProp = UserProperties.getInstance();
		actionController = new ActionController();
		
		gridPanel = new GridPanel(sudokuGame.getSudokuGrid());
		busyComponent = new JBusyComponent<GridPanel>(gridPanel);
		getContentPane().add(busyComponent, BorderLayout.CENTER);
		
		initFileChooser();
		initExportChooser();
		initActions();
		intiUI();

		addWindowListener(actionController);
		setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
		pack();
		setResizable(false);
		setFramePosition();
		
		//set initial state
		stateChanged(sudokuGame.getState(), sudokuGame.getState());
		sudokuGame.addObserver(this);
		
		setVisible(true);
	}
	
	private void initFileChooser() {
		String currentDir = ".";
		fileChooser = new JFileChooser(currentDir);
		fileChooser.setFileFilter(new FileFilter() {
			
			@Override
			public String getDescription() {
				return Globals.FILE_SUFFIX;
			}
			
			@Override
			public boolean accept(File file) {
				if (file != null) {
					return file.isDirectory()
							|| file.toString().endsWith(Globals.FILE_SUFFIX);
				} else {
					return false;
				}
			}
		});
	}
	
	private void initExportChooser() {
		String currentDir = ".";
		exportChooser = new JFileChooser(currentDir);
		exportChooser.setFileFilter(new FileFilter() {
			
			@Override
			public String getDescription() {
				return "png";
			}
			
			@Override
			public boolean accept(File file) {
				if (file != null) {
					return file.isDirectory()
							|| file.toString().endsWith(".png");
				} else {
					return false;
				}
			}
		});
	}

	private void initActions() {
		try {
			actionManager = ActionManager.getInstance();
			actionManager.loadActions(Globals.ACTIONS_URL);
		} catch (IOException e) {
			log.error(e.getMessage(), e);
		}
		
		// Register the callback methods on the actions
		actionManager.registerCallback("new-command", actionController, "handleNew");
		actionManager.registerCallback("open-command", actionController, "handleOpen");
		actionManager.registerCallback("save-command", actionController, "handleSave");
		actionManager.registerCallback("save-as-command", actionController, "handleSaveAs");
		actionManager.registerCallback("export-command", actionController, "handleExport");
		actionManager.registerCallback("exit-command", actionController, "handleExit");
		actionManager.registerCallback("reset-command", actionController, "handleReset");
		actionManager.registerCallback("clear-command", actionController, "handleClear");
		actionManager.registerCallback("check-command", actionController, "handleCheck");
		actionManager.registerCallback("solve-command", actionController, "handleSolve");
		actionManager.registerCallback("play-command", actionController, "handlePlay");
		actionManager.registerCallback("edit-command", actionController, "handleEdit");
		actionManager.registerCallback("about-command", actionController, "handleAbout");
		
		playAction = actionManager.getAction("play-command");
		clearAction = actionManager.getAction("clear-command");
		editAction = actionManager.getAction("edit-command");
		checkAction = actionManager.getAction("check-command"); 
		solveAction = actionManager.getAction("solve-command");
		resetAction = actionManager.getAction("reset-command");
	}
	
	private void intiUI() {
		//init menu bar
		UIFactory factory = UIFactory.getInstance();

		JMenuBar menubar = factory.createMenuBar("main-menu");
		if (menubar != null) {
		    setJMenuBar(menubar);
		} else {
			log.error("could not create menubar");
		}
		
		//init control panel
		controlPanel = new ControlPanel(playAction, solveAction, checkAction);
		getContentPane().add(controlPanel, BorderLayout.SOUTH);
	}
	
	private void setFramePosition() {
		Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
		
		int x = (screen.width - getSize().width) / 2;
		int y = (screen.height - getSize().height) / 2;
		
		setLocation(x, y);
	}
	
	@Override
	public void stateChanged(SudokuState oldState, SudokuState newState) {
		log.debug("new State " + newState);
		
		switch(newState) {
		case CREATING:
			playAction.setEnabled(true);
			clearAction.setEnabled(true);
			editAction.setEnabled(false);
			checkAction.setEnabled(false);
			solveAction.setEnabled(false);
			resetAction.setEnabled(false);
			gridPanel.setModifyPuzzleNumbers(true);
			controlPanel.showCard(newState);
			controlPanel.setInfoMessage(Globals.MSG.getString("create.info"));
			break;
		case PLAYING:
			playAction.setEnabled(false);
			clearAction.setEnabled(false);
			editAction.setEnabled(true);
			checkAction.setEnabled(true);
			solveAction.setEnabled(true);
			resetAction.setEnabled(true);
			gridPanel.setModifyPuzzleNumbers(false);
			controlPanel.showCard(newState);
			controlPanel.setInfoMessage(Globals.MSG.getString("play.info"));
			break;
		case SOLVING:
			playAction.setEnabled(false);
			clearAction.setEnabled(false);
			editAction.setEnabled(false);
			checkAction.setEnabled(false);
			solveAction.setEnabled(false);
			resetAction.setEnabled(false);
			controlPanel.setInfoMessage(Globals.MSG.getString("solving.info"));
			break;
			
		}
	}
	
	/**
	 * This class handles all user inputs.
	 * 
	 * @author amsler
	 */
	public class ActionController extends WindowAdapter {
		
		private int[][] lastSavedGrid = sudokuGame.getSudokuGrid().asArray(true);
		
		
		
		@Override
		public void windowClosing(WindowEvent e) {
			handleExit();
		}
		
		/**
		 * Create a new Sudoku
		 */
		public void handleNew() {
			if (handleSaveConfirmDialog()) {
				sudokuGame.newGame();
				gridPanel.repaint();
			}
		}
		
		/**
		 * Open an existing Sudoku.
		 */
		public void handleOpen() {
			if (!handleSaveConfirmDialog()) {
				return;
			}
			
			File currentDirectory = new File(userProp.getLastGame());
			fileChooser.setCurrentDirectory(currentDirectory);
			
			int state = fileChooser.showOpenDialog(SudokuFrame.this);
			
			if (state == JFileChooser.APPROVE_OPTION ) {
				File file = fileChooser.getSelectedFile();
				boolean success = sudokuGame.load(file.getAbsolutePath());
				if (success) {
					userProp.setLastGame(file.getAbsolutePath());
					lastSavedGrid = sudokuGame.getSudokuGrid().asArray(true);
					
					//update gui
					gridPanel.setSudokuGrid(sudokuGame.getSudokuGrid());
					gridPanel.repaint();
				} else {
					JOptionPane.showMessageDialog(SudokuFrame.this,
							Globals.MSG.getString("load.error"));
				}
			}
		}
		
		/**
		 * Save the current Sudoku.
		 */
		public void handleSave() {
			String gameId = sudokuGame.getGameId();
			File savedGame = new File(gameId);
			
			if (savedGame.exists() && savedGame.canWrite()) {
				if (sudokuGame.save(gameId)) {
					lastSavedGrid = sudokuGame.getSudokuGrid().asArray(true);
				} else {
					JOptionPane.showMessageDialog(SudokuFrame.this,
							Globals.MSG.getString("save.error"));
				}
			} else {
				handleSaveAs();
			}
		}
		
		/**
		 * Save the current Sudoku.
		 */
		public void handleSaveAs() {
			File currentDirectory = new File(userProp.getLastGame());
			fileChooser.setCurrentDirectory(currentDirectory);
			fileChooser.setSelectedFile(new File(Globals.DEFAULT_SSK_FILE_NAME));
			
			int state = fileChooser.showSaveDialog(SudokuFrame.this);
			
			if (state == JFileChooser.APPROVE_OPTION ) {
				File file = fileChooser.getSelectedFile();
				
				//add suffix if not exist
				String path = file.getPath();
				if (!path.endsWith(Globals.FILE_SUFFIX)) {
					file = new File(path + Globals.FILE_SUFFIX);
				}
				
				//ask user if file should be overwritten 
				if (file.exists()) {
					int option = JOptionPane.showConfirmDialog(
							SudokuFrame.this,
							Globals.MSG.getString("overwrite.dialog.msg"),
							Globals.MSG.getString("overwrite.dialog.title"),
							JOptionPane.YES_NO_OPTION);
					if (option == JOptionPane.NO_OPTION) {
						return;
					}
				}
				
				boolean success = sudokuGame.save(file.getAbsolutePath());
				if (success) {
					userProp.setLastGame(file.getAbsolutePath());
					lastSavedGrid = sudokuGame.getSudokuGrid().asArray(true);
				} else {
					JOptionPane.showMessageDialog(SudokuFrame.this,
							Globals.MSG.getString("save.error"));
				}
			}
		}
		
		/**
		 * Save the current Sudoku as image.
		 */
		public void handleExport() {
			File currentDirectory = new File(userProp.getLastExportDir());
			exportChooser.setCurrentDirectory(currentDirectory);
			exportChooser.setSelectedFile(new File("sudoku.png"));
			
			int state = exportChooser.showSaveDialog(SudokuFrame.this);
			if (state == JFileChooser.APPROVE_OPTION ) {
				File file = exportChooser.getSelectedFile();
				
				//ask user if file should be overwritten 
				if (file.exists()) {
					int option = JOptionPane.showConfirmDialog(
							SudokuFrame.this,
							Globals.MSG.getString("overwrite.dialog.msg"),
							Globals.MSG.getString("overwrite.dialog.title"),
							JOptionPane.YES_NO_OPTION);
					if (option == JOptionPane.NO_OPTION) {
						return;
					}
				}
				
				SudokuExporter exporter = new SudokuExporter(gridPanel.getImage());
				exporter.writeImage(ImageFormat.PNG, file);
				userProp.setLastExportDir(exportChooser.getCurrentDirectory().getAbsolutePath());
			}

		}
		
		/**
		 * Close the application.
		 */
		public void handleExit() {
			if (handleSaveConfirmDialog()) {
				log.debug("exit application");
				userProp.save();
				System.exit(0);
			}
		}
		
		/**
		 * Play the created Sudoku.
		 */
		public void handlePlay() {
			SudokuGrid grid = sudokuGame.getSudokuGrid();
			
			if (!grid.checkRules() || grid.getPuzzleNumberCount() < 17) {
				int option = JOptionPane.showConfirmDialog(
						SudokuFrame.this,
						Globals.MSG.getString("play.dialog.msg"),
						Globals.MSG.getString("play.dialog.title"),
						JOptionPane.YES_NO_OPTION);
				if (option == JOptionPane.NO_OPTION) {
					return;
				}
			}
			
			sudokuGame.play();
		}
		
		/**
		 * Edit the puzzle numbers.
		 */
		public void handleEdit() {
			SudokuGrid grid = sudokuGame.getSudokuGrid();
			
			if (grid.getSolutionNumberCount() > 0) {
				int option = JOptionPane.showConfirmDialog(
						SudokuFrame.this,
						Globals.MSG.getString("edit.dialog.msg"),
						Globals.MSG.getString("edit.dialog.title"),
						JOptionPane.YES_NO_OPTION);
				if (option == JOptionPane.NO_OPTION) {
					return;
				}
			}
			
			sudokuGame.edit();
			gridPanel.repaint();
		}
		
		/**
		 * Clear all numbers except of the puzzle numbers.
		 */
		public void handleReset() {
			SudokuGrid sudokuGrid = sudokuGame.getSudokuGrid();
			sudokuGrid.reset();
			gridPanel.repaint();
			log.debug("reset");
			log.debug(sudokuGame);
		}
		
		/**
		 * Clear all numbers.
		 */
		public void handleClear() {
			SudokuGrid sudokuGrid = sudokuGame.getSudokuGrid();
			sudokuGrid.clear();
			gridPanel.repaint();
			log.debug("clear");
			log.debug(sudokuGame);
		}
		
		/**
		 * Check the sudoku rules.
		 */
		public void handleCheck() {
			log.debug("check");
			log.debug(sudokuGame);
			
			SudokuGrid grid = sudokuGame.getSudokuGrid();
			
			if (!grid.isCompleted()) {
				controlPanel.setInfoMessage(Globals.MSG.getString("check.uncompleted.info"));
				return;
			}
			
			boolean isValid = grid.checkRules();
			if (isValid) {
				controlPanel.setInfoMessage(Globals.MSG.getString("check.rules.valid"));
			} else {
				controlPanel.setInfoMessage(Globals.MSG.getString("check.rules.invalid"));
			}
			repaint();
		}
		
		/**
		 * Solve the Sudoku completely.
		 */
		public void handleSolve() {
			SudokuGrid grid = sudokuGame.getSudokuGrid();
			
			if (!grid.checkRules() || grid.isEmpty()) {
				controlPanel.setInfoMessage(Globals.MSG.getString("check.rules.invalid"));
				repaint();
				return;
			}
			
			if (grid.isCompleted()) {
				return;
			}
			
			//start new Thread for solving algorithm 
			new Thread(new Runnable() {
				
				public void run() {
					try {
						busyComponent.setBusy(true);
						boolean success = sudokuGame.solve();
						if (success) {
							controlPanel.setInfoMessage(Globals.MSG.getString("solving.solution.found"));
							gridPanel.repaint();
						} else {
							controlPanel.setInfoMessage(Globals.MSG.getString("solving.solution.not.found"));
						}
					} finally {
						busyComponent.setBusy(false);
					}
				}
			}).start();
		}
		
		/**¨
		 * Show the info dialog.
		 */
		public void handleAbout() {
			new InfoDialog(SudokuFrame.this);
			log.debug("handle about");
		}
		
		private boolean handleSaveConfirmDialog() {
			SudokuGrid grid = sudokuGame.getSudokuGrid();
			int[][] currentGrid = grid.asArray(true);
			
			if (!Arrays.deepEquals(currentGrid, lastSavedGrid) && !grid.isEmpty()) {
				int option = JOptionPane.showConfirmDialog(SudokuFrame.this,
						Globals.MSG.getString("save.confirm.dialog"));
				
				if (option == JOptionPane.CANCEL_OPTION) {
					return false;
				} else if (option == JOptionPane.OK_OPTION) {
					handleSave();
				} 
			}
			
			return true;
		}
	}

}
